﻿namespace NJQuery {
	using System;
	using System.Web.UI;
	using System.Web.UI.WebControls;
	using System.ComponentModel;
	using System.Globalization;
	using System.Collections.Specialized;
	using System.Collections.Generic;
	using System.Diagnostics.CodeAnalysis;

	/// <summary>
	/// Server control used to generate a <see href="http://jqueryui.com/demos/accordion/">jQuery UI Accordion widget</see>.
	/// </summary>
	[ParseChildren(true), PersistChildren(false)]
	public class Accordion : ManagedWebControl, IPostBackDataHandler, IPostBackEventHandler {


		#region Constants: Property defaults

		private const int defaultActive = 0;
		private const string defaultAnimatedString = "slide";
		private const bool defaultAutoHeight = true;
		private const bool defaultClearStyle = false;
		private const bool defaultCollapsible = false;
		private const PanelActivateEvent defaultEvent = PanelActivateEvent.Click;
		private const bool defaultFillSpace = false;
		private const AccordionHeaderTag defaultHeaderTag = AccordionHeaderTag.Div;
		private const bool defaultNavigation = false;

		#endregion


		#region Static Fields: Property defaults

		private static readonly AccordionAnimation defaultAnimated = AccordionAnimation.Slide;

		#endregion


		#region Fields

		private AccordionIcons icons;
		private AccordionPanelCollection panelCollection;
		private AccordionAnimationDef animatedControl;
		private AccordionPanel pendingActive;
		private bool activatePending;
		private bool activatePendingByPostBack;

		#endregion


		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="Accordion"/> class.
		/// </summary>
		public Accordion() : base(HtmlTextWriterTag.Div) { }

		#endregion


		#region Properties: Widget initialization options

		/// <summary>
		/// Gets or sets the currently active panel.  If not set and <see cref="Collapsible"/> = false, the first panel will be rendered as active.
		/// The visible index of the selected control (or false for null) is mapped to the "active" option when the jQuery UI widget is initialized.
		/// </summary>
		public virtual AccordionPanel Active {
			get {
				foreach(AccordionPanel panel in Panels) if(panel.Active) return panel;
				return null;
			}
			set {
				foreach(AccordionPanel panel in Panels)
					panel.SetActive(panel == value);
			}
		}

		/// <summary>
		/// Gets or sets the animation used when the user switches active panels.
		/// This property maps the "animated" option (using false in place None/empty constructor value) when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(typeof(AccordionAnimation), defaultAnimatedString), Category("Behavior")]
		public virtual AccordionAnimation Animated {
			get { return ViewState["Animated"] as AccordionAnimation? ?? defaultAnimated; }
			set { ViewState["Animated"] = value; }
		}

		/// <summary>
		/// If set, the highest content part is used as height reference for all other parts. Provides more consistent animations.
		/// This property maps directly to the "autoHeight" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <seealso cref="ClearStyle"/>
		[DefaultValue(defaultAutoHeight), Category("Behavior")]
		public virtual bool AutoHeight {
			get { return ViewState["AutoHeight"] as bool? ?? defaultAutoHeight; }
			set { ViewState["AutoHeight"] = value; }
		}

		/// <summary>
		/// If set, clears height and overflow styles after finishing animations. This enables accordions to work with dynamic content. Won't work together with <see cref="AutoHeight"/>.
		/// This property maps directly to the "clearStyle" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <see cref="AutoHeight"/>
		[DefaultValue(defaultClearStyle), Category("Behavior")]
		public virtual bool ClearStyle {
			get { return ViewState["ClearStyle"] as bool? ?? defaultClearStyle; }
			set { ViewState["ClearStyle"] = value; }
		}

		/// <summary>
		/// Whether all the sections can be closed at once. Allows collapsing the active section by the triggering event.
		/// This property maps directly to the "collapsible" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(defaultCollapsible), Category("Behavior")]
		public virtual bool Collapsible {
			get { return ViewState["Collapsible"] as bool? ?? defaultCollapsible; }
			set { ViewState["Collapsible"] = value; }
		}

		/// <summary>
		/// The event on which to trigger the accordion.
		/// This property maps directly to the "event" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <exception cref="ArgumentException">Value not defined.</exception>
		[DefaultValue(defaultEvent), Category("Behavior")]
		[SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Event", Justification = "Exposed as the jQuery option name.")]
		public virtual PanelActivateEvent Event {
			get { return ViewState["Event"] as PanelActivateEvent? ?? defaultEvent; }
			set {
				if(!Enum.IsDefined(typeof(PanelActivateEvent), value)) throw new ArgumentException("Value not defined.", "value");
				ViewState["Event"] = value;
			}
		}

		/// <summary>
		/// If set, the accordion completely fills the height of the parent element. Overrides <see cref="AutoHeight"/>.
		/// This property maps directly to the "fillSpace" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(defaultFillSpace), Category("Behavior")]
		public virtual bool FillSpace {
			get { return ViewState["FillSpace"] as bool? ?? defaultFillSpace; }
			set { ViewState["FillSpace"] = value; }
		}

		/// <summary>
		/// Selector for the header element.
		/// Controls the HTML tag that will be used to render the <see cref="AccordionPanel.Header"/> property on the contained <see cref="AccordionPanel"/> controls.
		/// This property maps directly to the "header" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <exception cref="ArgumentException">Value not defined.</exception>
		[DefaultValue(defaultHeaderTag), Category("Layout")]
		public virtual AccordionHeaderTag HeaderTag {
			get { return ViewState["Header"] as AccordionHeaderTag? ?? defaultHeaderTag; }
			set {
				if(!Enum.IsDefined(typeof(AccordionHeaderTag), value)) throw new ArgumentException("Value not defined.", "value");
				ViewState["Header"] = value;
			}
		}

		/// <summary>
		/// The icons to use for headers.
		/// This property maps directly to the "icons" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(null), MergableProperty(false), PersistenceMode(PersistenceMode.InnerProperty), Category("Appearance")]
		public virtual AccordionIcons Icons {
			get {
				if(icons == null) icons = CreateIcons();
				return icons;
			}
		}

		/// <summary>
		/// Gets or sets whether to look for the anchor that matches location.href and activate it.  This is great for href-based state-saving.  Use navigationFilter to implement your own matcher.
		/// This property maps directly to the "navigation" option when the jQuery UI widget is initialized.
		/// </summary>
		/// <seealso cref="NavigationFilter"/>
		[DefaultValue(defaultNavigation), Category("Behavior")]
		public virtual bool Navigation {
			get { return ViewState["Navigation"] as bool? ?? defaultNavigation; }
			set { ViewState["Navigation"] = value; }
		}

		/// <summary>
		/// Gets or sets a client-side JavaScript function to overwrite the default location.href-matching with a custom matcher.
		/// This property maps directly to the "navigationFilter" option when the jQuery UI widget is initialized.
		/// </summary>
		[DefaultValue(""), Category("Behavior")]
		public virtual string NavigationFilter {
			get { return ViewState["NavigationFilter"] as string ?? string.Empty; }
			set { ViewState["NavigationFilter"] = value; }
		}

		/// <summary>
		/// This event is triggered every time the accordion changes. If the accordion is animated, the event will be triggered upon completion of the animation; otherwise, it is triggered immediately.
		/// This is a client-side JavaScript event with the signature function(event,ui).
		/// The property appends itself to code used to persist the <see cref="Active"/> property and maps to the "change" option with the jQuery widget is initalized.
		/// </summary>
		/// <seealso cref="OnClientChangeStart"/>
		[DefaultValue(""), Category("Behavior")]
		public virtual string OnClientChange {
			get { return ViewState["OnClientChange"] as string ?? string.Empty; }
			set { ViewState["OnClientChange"] = value; }
		}

		/// <summary>
		/// This event is triggered every time the accordion starts to change.
		/// This is a client-side JavaScript event with the signature function(event,ui).
		/// This property will prepend any necessary ASP.NET postback code and be mapped to the "changestart" option when the jQuery UI widget is initialized or afterward as needed.
		/// </summary>
		/// <seealso cref="ChangeStart"/>"/>
		/// <seealso cref="PostBackOnChangeStart"/>
		/// <seealso cref="OnClientChange"/>
		[DefaultValue(""), Category("Behavior")]
		public virtual string OnClientChangeStart {
			get { return ViewState["OnClientChangeStart"] as string ?? string.Empty; }
			set { ViewState["OnClientChangeStart"] = value; }
		}

		#endregion


		#region Properties: Additional

		/// <summary>
		/// Gets or sets whether or not the control performs a postback in the widget's "changestart" event.
		/// A True value is required in order to receive a <see cref="ChangeStart"/> event.
		/// </summary>
		/// <seealso cref="OnClientChangeStart"/>
		[DefaultValue(false), Category("Behavior")]
		public virtual bool PostBackOnChangeStart {
			get { return ViewState["PostBackOnChangeStart"] as bool? ?? false; }
			set { ViewState["PostBackOnChangeStart"] = value; }
		}

		/// <summary>
		/// Gets the child <see cref="AccordionPanel"/> controls to be rendered within the <see cref="Accordion"/> widget.
		/// </summary>
		[DefaultValue(null), MergableProperty(false), PersistenceMode(PersistenceMode.InnerProperty), Category("Misc")]
		public virtual AccordionPanelCollection Panels {
			get {
				EnsureChildControls();
				return panelCollection; 
			}
		}

		/// <summary>
		/// Gets the rendered element ID of the hidden field used to round-trip the <see cref="Active"/> property.
		/// </summary>
		public virtual string ActiveClientId {
			get { return string.Format(CultureInfo.InvariantCulture, "__{0}_Active", ClientID); }
		}

		#endregion


		#region Properties: Widget methods

		/// <summary>
		/// Gets whether or not the <see cref="O:NJQuery.Accordion.Activate"/> method has been called, causing the client "activate" method to be rendered and executed on load.
		/// </summary>
		/// <seealso cref="PendingActive"/>
		/// <seealso cref="CancelActivate"/>
		public virtual bool ActivatePending {
			get { return activatePending; }
		}

		/// <summary>
		/// Gets the panel that will be activated via client code on render (if applicable).
		/// </summary>
		/// <seealso cref="ActivatePending"/>
		/// <seealso cref="O:NJQuery.Accordion.Activate"/>
		/// <seealso cref="CancelActivate"/>
		public virtual AccordionPanel PendingActive {
			get { return pendingActive; }
		}

		#endregion


		#region Properties: Protected

		/// <summary>
		/// Gets the event identifier prepended to the activated panel's client ID when posted back in the client's "changestart" event.
		/// </summary>
		protected virtual string ChangeStartEventArgPrefix {
			get { return "ChangeStart:"; }
		}

		/// <summary>
		/// Gets whether or not postback code should be rendered in the client's "changestart" event.
		/// </summary>
		protected virtual bool RenderPostBackOnChangeStart {
			get {
				if(PostBackOnChangeStart) return true;
				foreach(AccordionPanel panel in Panels)
					if(panel.Visible && panel.PostBackOnChangeStart) return true;
				return false;
			}
		}

		#endregion


		#region Events

		/// <summary>
		/// Occurs on postback when the <see cref="Active"/> property value has changed since previous render.
		/// </summary>
		[Category("Action")]
		public event EventHandler ActiveChanged;

		/// <summary>
		/// This event is triggered every time the accordion starts to change.
		/// This event requires the <see cref="PostBackOnChangeStart" /> property (or one or more contained <see cref="AccordionPanel.PostBackOnChangeStart">AccordionPanel.PostBackOnChangeStart</see> properties) to have a value of True.
		/// The ASP.NET postback will be appended to any <see cref="OnClientChangeStart"/> code and mapped to the "changestart" option after the jQuery UI widget is initialized.
		/// </summary>
		[Category("Action")]
		public event EventHandler<AccordionChangeStartEventArgs> ChangeStart;

		#endregion


		#region Methods: Event hooks

		/// <summary>
		/// Raises the <see cref="ActiveChanged"/> event.
		/// </summary>
		/// <param name="e">The <see cref="EventArgs"/> object that contains the event data.</param>
		protected virtual void OnActiveChanged(EventArgs e) {
			if(ActiveChanged != null) ActiveChanged.Invoke(this, e);
		}

		/// <summary>
		/// Raises the <see cref="ChangeStart"/> event.
		/// </summary>
		/// <param name="e">The <see cref="AccordionChangeStartEventArgs"/> object that contains the event data.</param>
		protected virtual void OnChangeStart(AccordionChangeStartEventArgs e) {
			if(ChangeStart != null) ChangeStart.Invoke(this, e);
		}

		#endregion


		#region Methods: Widget methods

		/// <summary>
		/// Activate a panel of the accordion programmatically.
		/// This method will cause a client-side jQuery call to the widget's "activate" method to be rendered and executed at page load.
		/// This is similar to setting the <see cref="Active"/> property on the control or panel with the primary difference being this method will defer the change until client page load and also display using any applicable animations.
		/// </summary>
		/// <param name="index">The index can be a zero-based number to match the position of the panel.  Pass -1 to close all (only possible with <see cref="Collapsible"/> = true).</param>
		/// <seealso cref="Active"/>
		/// <exception cref="ArgumentOutOfRangeException">Value must greater or equal to -1 and less than the value returned by Panels.Count.</exception>
		public void Activate(int index) {
			AccordionPanelCollection panels = Panels;
			if(index < -1 || index >= panels.Count) throw new ArgumentOutOfRangeException("index", index, "Value must greater or equal to -1 and less than the value returned by Panels.Count.");

			if(index == -1) Activate(null);
			else Activate(panels[index]);
		}

		/// <summary>
		/// Activate a panel of the accordion programmatically.
		/// This method will cause a client-side jQuery call to the widget's "activate" method to be rendered and executed at page load.
		/// This is similar to setting the <see cref="Active"/> property on the control or panel with the primary difference being this method will defer the change until client page load and also display using any applicable animations.
		/// </summary>
		/// <param name="panel">The panel to activate.  Pass null to close all (only possible with <see cref="Collapsible"/> = true).</param>
		/// <seealso cref="ActivatePending"/>
		/// <seealso cref="PendingActive"/>
		/// <seealso cref="CancelActivate"/>
		public virtual void Activate(AccordionPanel panel) {
			if(activatePendingByPostBack && pendingActive == panel) return;
			pendingActive = panel;
			activatePending = true;
			activatePendingByPostBack = false;
		}

		/// <summary>
		/// Cancels a prior call to the <see cref="O:NJQuery.Accordion.Activate"/> method.
		/// </summary>
		/// <seealso cref="ActivatePending"/>
		/// <seealso cref="PendingActive"/>
		public virtual void CancelActivate() {
			pendingActive = null;
			activatePending = false;
			activatePendingByPostBack = false;
		}

		#endregion


		#region Methods: Child controls

		/// <summary>
		/// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparation for posting back or rendering.
		/// </summary>
		protected override void CreateChildControls() {
			base.CreateChildControls();

			if(panelCollection == null) {
				GroupControlContainer panelContainer = CreatePanelContainer();
				GroupedControls.AddContainer(panelContainer);
				panelCollection = CreatePanels(panelContainer);
			}

		}

		/// <summary>
		/// Creates a <see cref="AccordionIcons"/> object to be used as the <see cref="Icons"/> property.
		/// </summary>
		/// <returns>A new <see cref="AccordionIcons"/> instance.</returns>
		protected virtual AccordionIcons CreateIcons() {
			return new AccordionIcons(ViewState);
		}

		/// <summary>
		/// Creates a <see cref="GroupControlContainer"/> object to contain the grouped child controls for the <see cref="Panels"/> property.
		/// </summary>
		/// <returns></returns>
		protected virtual GroupControlContainer CreatePanelContainer() {
			return new GroupControlContainer(this);
		}

		/// <summary>
		/// Creates a <see cref="AccordionPanelCollection"/> object to be used as the <see cref="Panels"/> property.
		/// </summary>
		/// <returns>A new <see cref="AccordionPanelCollection"/> instance.</returns>
		protected virtual AccordionPanelCollection CreatePanels(GroupControlContainer container) {
			return new AccordionPanelCollection(container);
		}

		#endregion


		#region Methods: Rendering hooks

		/// <summary>
		/// Raises the <see cref="Control.PreRender"/> event.
		/// </summary>
		/// <param name="e">The <see cref="EventArgs"/> object that contains the event data.</param>
		protected override void OnPreRender(EventArgs e) {
			base.OnPreRender(e);

			string animatedControlId = Animated.ControlId;
			if(!string.IsNullOrEmpty(animatedControlId)) animatedControl = NamingContainer.FindControl(animatedControlId) as AccordionAnimationDef;

			if(Visible) Page.RegisterRequiresPostBack(this);
		}

		/// <summary>
		/// Allows a jQuery server control to reference scripts and stylesheets necessary to render their client markup and widgets.
		/// </summary>
		/// <param name="manager">The <see cref="Manager"/> used to handle style and script referencing in addition to unified generation of load and ready scripts.</param>
		protected override void Register(Manager manager) {
			AccordionStandardAnimation? animatedStandardAnimation = Animated.StandardAnimation;

			manager.RegisterClientScript(ClientScript.Core);
			manager.RegisterClientScript(ClientScript.UIWidgetFactory);
			manager.RegisterClientScript(ClientScript.UIAccordionWidget);
			if(animatedStandardAnimation.HasValue) {
				switch(animatedStandardAnimation.Value) {
					case AccordionStandardAnimation.BounceSlide:
						manager.RegisterClientScript(ClientScript.UIEffectsCore);
						break;
					case AccordionStandardAnimation.EaseSlide:
						manager.RegisterClientScript(ClientScript.UIEffectsCore);
						manager.RegisterClientScript(ClientScript.EasingPlugin);
						break;
				}
			}

			manager.RegisterClientStyle(ClientStyle.UICore);
			manager.RegisterClientStyle(ClientStyle.UIAccordionWidget);
			manager.RegisterTheme();

			manager.RegisterForReadyScript(this);
		}

		/// <summary>
		/// Adds HTML attributes and styles that need to be rendered to the specified <see cref="HtmlTextWriter"/>.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="writer">A <see cref="HtmlTextWriter"/> that represents the output stream to render HTML content on the client.</param>
		protected override void AddAttributesToRender(HtmlTextWriter writer) {

			//TODO: Let's revisit the whole "degradability versus getting rid of flicker issue"  I don't think this falls under UseCssStyling, perhaps a DegradeForNonJs... Or is that a moot point here?
			if(!MasterUseCssStyling)
				writer.AddStyleAttribute("display", "none");

			base.AddAttributesToRender(writer);
		}

		/// <summary>
		/// Sends server control content to a provided <see cref="HtmlTextWriter"/> object, which writes the content to be rendered to the client.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="writer">A <see cref="HtmlTextWriter"/> that represents the output stream to render HTML content on the client.</param>
		protected override void Render(HtmlTextWriter writer) {

			ControlCollection controls = panelCollection.Container.Controls;
			for(int i = 1; i < controls.Count; i+=2) {
				AccordionPanel panel = controls[i] as AccordionPanel;
				if(panel == null || !panel.Visible) continue;

				LiteralControl literal = controls[i - 1] as LiteralControl;
				if(literal == null) continue;

				literal.Text = string.Format(CultureInfo.InvariantCulture,
					"<{0}><a href=\"#\">{1}</a></{0}>",
					ScriptBuilder.GetCamelCaseValue(HeaderTag),
					panel.Header);

			}


			base.Render(writer);

			ClientScriptManager clientScript = Page.ClientScript;
			clientScript.RegisterForEventValidation(new PostBackOptions(this));

			AccordionPanel active = Active;
			clientScript.RegisterHiddenField(ActiveClientId, active == null ? string.Empty : active.ClientID);
		}

		/// <summary>
		/// Renders the jQuery "Ready Event" script required to generate the client-side jQuery widget.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected override void RenderReadyScript(ScriptBuilder script) {
			string clientId = ClientID;

			//TODO: Let's revisit the whole "degradability versus getting rid of flicker issue"  I don't think this falls under UseCssStyling, perhaps a DegradeForNonJs... Or is that a moot point here?
			if(!MasterUseCssStyling) {
				script.NewLine();
				script.RenderIdSelector(clientId);
				script.Append(".css('display', ''); ");
			}

			RenderInitScript(script);
			if(!IsEnabled) RenderDisableScript(script);
			if(activatePending) RenderActivateScript(script);
			if(RenderPostBackOnChangeStart) RenderChangeStartPostBackScript(script);
		}

		/// <summary>
		/// Renders the jQuery UI initialization script.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderInitScript(ScriptBuilder script) {

			script.NewLine();

			script.RenderIdSelector(ClientID);
			script.Append(".accordion(");

			PropertyCollection properties = script.CreateProperties();
			AddInitProperties(properties);
			script.RenderProperties(properties);

			script.Append(");");
		}

		/// <summary>
		/// Creates the properties object that will be rendered as the jQuery UI initialization options.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="properties">A <see cref="PropertyCollection"/> object that will become the jQuery UI initialization options.</param>
		protected virtual void AddInitProperties(PropertyCollection properties) {

			int? active = GetActiveVisibleIndex();
			bool collapsible = Collapsible;
			if(active == null && collapsible) properties.AddValue("active", false);
			else if(active.HasValue && active.Value != defaultActive) properties.AddValue("active", active.Value);

			AccordionAnimation animated = Animated;
			if(animatedControl != null) properties.AddValue("animated", animatedControl.Name);
			else if(animated != defaultAnimated) properties.AddValue("animated", animated);

			bool autoHeight = AutoHeight;
			if(autoHeight != defaultAutoHeight) properties.AddValue("autoHeight", autoHeight);

			bool clearStyle = ClearStyle;
			if(clearStyle != defaultClearStyle) properties.AddValue("clearStyle", clearStyle);

			if(collapsible != defaultCollapsible) properties.AddValue("collapsible", collapsible);

			PanelActivateEvent @event = Event;
			if(@event != defaultEvent) properties.AddEnumValue("event", @event);

			bool fillSpace = FillSpace;
			if(fillSpace != defaultFillSpace) properties.AddValue("fillSpace", fillSpace);

			properties.AddValue("header", HeaderTag.GetSelector());

			if(icons != null && !icons.IsDefault) properties.AddValue("icons", icons);

			bool navigation = Navigation;
			if(navigation != defaultNavigation) properties.AddValue("navigation", navigation);

			string navigationFilter = NavigationFilter;
			if(!string.IsNullOrEmpty(navigationFilter)) properties.AddValue("navigationFilter", navigationFilter);

			ScriptBuilder changeScript = properties.CreateChildScript();
			RenderChangeScript(changeScript);
			if(changeScript.Length != 0) properties.AddScript("change", changeScript);

			//If server event is hooked up, this will happen later, but it must happen here if it's an activate that's not already in progress across a postback
			if(!RenderPostBackOnChangeStart || (activatePending && !activatePendingByPostBack)) {
				string onClientChangeStart = OnClientChangeStart;
				if(!string.IsNullOrEmpty(onClientChangeStart)) properties.AddScript("changestart", onClientChangeStart);
			}

		}

		/// <summary>
		/// Renders the client "change" event script.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderChangeScript(ScriptBuilder script) {

			script.Append("function(event, ui) {");

			script.Indent++;
			script.NewLine();

			//Persist Active property
			script.RenderIdSelector(ActiveClientId);
			script.Append(".val(ui.newHeader[0] == undefined ? '' : $(ui.newHeader[0]).next().attr('id'));");

			//Add custom change event
			string onClientChange = OnClientChange ?? string.Empty;
			if(onClientChange.Length != 0) {

				script.NewLine();
				script.Append("var change = ");
				script.Append(onClientChange);
				script.Append(";");

				script.NewLine();
				script.Append("change(event, ui);");

			}

			script.Indent--;
			script.NewLine();

			script.Append("}");
		}

		/// <summary>
		/// Renders a client "disable" method call when the <see cref="WebControl.Enabled"/> property is set to false.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderDisableScript(ScriptBuilder script) {

			script.NewLine();

			script.RenderIdSelector(ClientID);
			script.Append(".accordion(");
			script.AppendValue("disable");
			script.Append(");");
		}

		/// <summary>
		/// Renders a client "activate" method call when a call is made server-side to the <see cref="O:NJQuery.Accordion.Activate"/> method.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderActivateScript(ScriptBuilder script) {

			int? activeVisibleIndex = GetActiveVisibleIndex();
			int? activateVisibleIndex = GetVisibleIndex(PendingActive);

			if(activateVisibleIndex == activeVisibleIndex) return;
			if(activateVisibleIndex == null && !Collapsible) return;

			script.NewLine();

			script.RenderIdSelector(ClientID);
			script.Append(".accordion(");
			script.AppendValue("activate");
			script.Append(", ");
			if(activateVisibleIndex.HasValue) script.AppendValue(activateVisibleIndex.Value);
			else script.AppendValue(false);
			script.Append(");");

		}

		/// <summary>
		/// Renders the client "changestart" event script used to generate a postback.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A <see cref="ScriptBuilder"/> that creates the output stream to render jQuery JavaScript on the client.</param>
		protected virtual void RenderChangeStartPostBackScript(ScriptBuilder script) {
			string onClientChangeStart = OnClientChangeStart ?? string.Empty;

			script.NewLine();

			script.RenderIdSelector(ClientID);
			script.Append(".accordion('option', 'changestart', function(event, ui) {");

			script.Indent++;

			//Add custom changestart event
			if(onClientChangeStart.Length != 0) {

				script.NewLine();
				script.Append("var changestart = ");
				script.Append(onClientChangeStart);
				script.Append(";");

				script.NewLine();
				script.Append("changestart(event, ui);");

			}

			//If a cancel on the changestart event is ever added, the following code can be less hackish and greatly simplified
			string uniqueID = UniqueID;
			string prefix = ChangeStartEventArgPrefix;
			string headerSelector = HeaderTag.GetSelector();
			bool allPanelsPostBack = PostBackOnChangeStart;

			//Get list of individual panels with Activating event handlers if needed
			List<AccordionPanel> postBackPanels = allPanelsPostBack ? null : GetPostBackPanels();

			if(allPanelsPostBack || postBackPanels.Count != 0) {

				//Get new selected panel ID
				script.NewLine();
				script.Append("var newId = ui.newHeader.length == 0 ? '' : $(ui.newHeader[0]).next().attr('id');");

				//Create if block for selective postback
				if(!allPanelsPostBack) {
					script.NewLine();
					script.Append("if(");
					bool first = true;
					foreach(AccordionPanel panel in postBackPanels) {
						if(first)
							first = false;
						else
							script.Append(" || ");
						script.Append("newId == ");
						script.AppendValue(panel.ClientID);
					}
					script.Append(") {");
					script.Indent++;
				}

				//Calculate the old index (-1 for none)
				script.NewLine();
				script.Append("var oldIndex = ui.oldHeader.length == 0 ? -1 : $(this).find(");
				script.AppendValue(headerSelector);
				script.Append(").index(ui.oldHeader[0]);");

				//Turn off animation so any forced activates back to old panel are not noticable
				if(Animated.IsAnimated) {
					script.NewLine();
					script.Append("$(this).accordion('option', 'animated', false);");
				}

				//Turn off changestart event
				script.NewLine();
				script.Append("$(this).accordion('option', 'changestart', function(event, ui) { });");

				//Modify change event to only set/maintain the old index
				script.NewLine();
				script.Append("$(this).accordion('option', 'change', function(event, ui) {");
				script.Indent++;
				script.NewLine();
				script.Append("var newIndex = ui.newHeader.length == 0 ? -1 : $(this).find(");
				script.AppendValue(headerSelector);
				script.Append(").index(ui.newHeader[0]);");
				script.NewLine();
				script.Append("if(newIndex != oldIndex) $(this).accordion('activate', oldIndex == -1 ? false : oldIndex);");
				script.Indent--;
				script.NewLine();
				script.Append("});");

				//Perform the postback
				script.NewLine();
				script.Append("__doPostBack("); //TODO: Decide if there is some delegate way to as .NET for the postback function and still generate code in event argument
				script.AppendValue(uniqueID);
				script.Append(", ");
				script.AppendValue(prefix);
				script.Append(" + newId);");

				//Close if block for selective postback
				if(!allPanelsPostBack) {
					script.Indent--;
					script.NewLine();
					script.Append("}");
				}

				//Register for Event Validation
				ClientScriptManager clientScriptManager = Page.ClientScript;
				if(allPanelsPostBack) {
					foreach(AccordionPanel panel in Panels)
						if(panel.Visible)
							clientScriptManager.RegisterForEventValidation(uniqueID, prefix + panel.ClientID);
					if(Collapsible) clientScriptManager.RegisterForEventValidation(uniqueID, prefix);
				} else {
					foreach(AccordionPanel panel in postBackPanels)
						clientScriptManager.RegisterForEventValidation(uniqueID, prefix + panel.ClientID);
				}

			}

			script.Indent--;
			script.NewLine();

			script.Append("});");

		}

		#endregion


		#region Methods: Private routines

		private int? GetActiveVisibleIndex() {
			int index = 0;
			foreach(AccordionPanel panel in Panels) {
				if(panel.Visible) {
					if(panel.Active) return index;
					index++;
				}
			}
			return null;
		}

		private int? GetVisibleIndex(AccordionPanel panel) {

			if(panel == null || !panel.Visible) return null;

			int index = 0;
			foreach(AccordionPanel comparePanel in Panels) {
				if(comparePanel.Visible) {
					if(panel == comparePanel) return index;
					index++;
				}
			}
			return null;
		}

		private List<AccordionPanel> GetPostBackPanels() {
			List<AccordionPanel> list = new List<AccordionPanel>();

			foreach(AccordionPanel panel in Panels)
				if(panel.Visible && panel.PostBackOnChangeStart)
					list.Add(panel);

			return list;
		}

		#endregion


		#region Methods: Post data/events

		/// <summary>
		/// Processes postback data for an ASP.NET server control.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="postDataKey">The key identifier for the control.</param>
		/// <param name="postCollection">The collection of all incoming name values.</param>
		/// <returns>True if the server control's state changes as a result of the postback; otherwise, false.</returns>
		protected virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) {

			Page.ClientScript.ValidateEvent(postDataKey);

			string newActiveId = postCollection[ActiveClientId] ?? string.Empty;
			bool collapsed = newActiveId.Length == 0;

			AccordionPanel newActive = collapsed ? null : Panels.FindByClientId(newActiveId);

			if(newActive == null && !collapsed) return false;

			AccordionPanel oldActive = Active;
			if(oldActive == newActive) return false;

			if(newActive != null) newActive.Active = true;
			else if(oldActive != null) oldActive.Active = false;

			return true;
		}

		/// <summary>
		/// Signals the server control to notify the ASP.NET application that the state of the control has changed.
		/// This method is used primarily by control developers.
		/// </summary>
		[SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "This method is amazing.  You get a complaint if you don't include the protected one, but when you do, it tells you to use events.  I included it simply because it gives the derrived control a chance to hook deeper.")]
		protected virtual void RaisePostDataChangedEvent() {
			OnActiveChanged(EventArgs.Empty);
		}

		/// <summary>
		/// Enables a server control to process an event raised when a form is posted to the server.
		/// </summary>
		/// <param name="eventArgument">A <see cref="String"/> that represents an optional event argument to be passed to the event handler.</param>
		[SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate", Justification = "This method is amazing.  You get a complaint if you don't include the protected one, but when you do, it tells you to use events.  I included it simply because it gives the derrived control a chance to hook deeper.")]
		protected virtual void RaisePostBackEvent(string eventArgument) {

			Page.ClientScript.ValidateEvent(UniqueID, eventArgument);

			string changeStartEventArgPrefix = ChangeStartEventArgPrefix;

			if(eventArgument.StartsWith(changeStartEventArgPrefix, StringComparison.Ordinal)) {

				bool collapsed = eventArgument.Length == changeStartEventArgPrefix.Length;
				if(collapsed && !Collapsible) return;

				AccordionPanel newActive = null;
				if(!collapsed) {
					newActive = Panels.FindByClientId(eventArgument.Substring(changeStartEventArgPrefix.Length));
					if(newActive == null) return;
				}

				AccordionPanel oldActive = Active;
				if((oldActive == null && newActive == null) || oldActive == newActive) return;

				bool cancel = false;

				{
					AccordionChangeStartEventArgs e = new AccordionChangeStartEventArgs(oldActive, newActive);
					OnChangeStart(e);
					if(e.Cancel) cancel = true;
				}

				if(!cancel && newActive != null) {
					AccordionPanelActivatingEventArgs e = new AccordionPanelActivatingEventArgs(oldActive);
					newActive.OnActivatingInternal(e);
					if(e.Cancel) cancel = true;
				}

				if(!cancel) {
					pendingActive = newActive;
					activatePending = true;
					activatePendingByPostBack = true;
				}

			}

		}

		#endregion


		#region Explicit Implementation: IPostBackDataHandler

		bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection postCollection) {
			return LoadPostData(postDataKey, postCollection);
		}

		void IPostBackDataHandler.RaisePostDataChangedEvent() {
			RaisePostDataChangedEvent();
		}

		#endregion


		#region Explicit Implementation: IPostBackEventHandler

		void IPostBackEventHandler.RaisePostBackEvent(string eventArgument) {
			RaisePostBackEvent(eventArgument);
		}

		#endregion


	}

}